<?php
/**
 * @version		$Id: bbcode.php 105 2008-10-02 22:39:19Z louis $
 * @package 	JXtended
 * @subpackage	HTML
 * @copyright	Copyright (C) 2008 JXtended LLC. All rights reserved.
 * @license		GNU General Public License
 */

defined('JPATH_BASE') or die;

/**
 * Source code is inspired by the Drupal BBCode module.
 *
 * Contributors to that module include:
 *  - Alastair Maw
 *  - Gabor Hojtsy
 *  - László Bácsi
 *  - Frank Naude
 */

/**
 * BBCode parsing/replacing class
 *
 * @package		JXtended
 * @subpackage	HTML
 * @version		1.0
 */
class JXBBCode extends JObject {

	/**
	 * Array of smiley mappings
	 *
	 * @access	public
	 * @var		array
	 */
	var $smilies = array (
		':)' => 'smile.gif',
		':(' => 'frown.gif',
		';)' => 'wink.gif',
		':D' => 'big_grin.gif',
		'8)' => 'cool.gif'
	);

	/**
	 * Configuration options array
	 *
	 * @access	private
	 * @var		array
	 */
	var $_options = array (
		'whitespace' => 1,
		'enable_smilies' => true,
		'smileyPath' => '/images/',
		'enable_autolinks' => true,
		'nofollow_links' => true,
		'enable_simpleformat' => true,
		'enable_extendedformat' => false,
		'enable_urls' => true,
		'enable_quotes' => true,
		'enable_abbreviations' => true,
		'enable_anchors' => false,
		'enable_php' => true,
		'enable_google' => true,
		'enable_wikipedia' => true,
		'enable_youtube' => true,
		'enable_images' => true,
		'enable_flash' => false,
		'enable_tables' => true,
		'enable_lists' => true,
		'enable_code' => true,
		'enable_emailcloaking' => false,
	);

	/**
	 * The internal text buffer for conversion
	 *
	 * @access	private
	 * @var		string
	 */
	var $_buffer = '';

	/**
	 * Object constructor
	 *
	 * @access	protected
	 * @param	array	$options	Configuration options array for setting object configuration
	 * @return	void
	 * @since	1.0
	 */
	function __construct($options = null)
	{
		if (!empty ($options)) {
			$this->setOptions($options);
		}
	}

	/**
	 * Returns a reference to the global JXBBCode object, only creating it
	 * if it doesn't already exist.
	 *
	 * This method must be invoked as:
	 * 		<pre>	$bbcode = &JXBBCode::getInstance();</pre>
	 *
	 * @access	public
	 * @param	array	$options	Configuration options array for setting object configuration.
	 * @return	object	The JXBBCode object.
	 * @since	1.0
	 */
	function & getInstance($options = null)
	{
		static $instance;

		if (!is_object($instance)) {
			$instance = new JXBBCode($options);
		}

		return $instance;
	}

	/**
	 * Method to set the configuration options for the object.
	 *
	 * @access	public
	 * @param	array	$options	Configuration options array to set for the object.
	 * @return	void
	 * @since	1.0
	 */
	function setOptions($options)
	{
		$this->_options = array_merge($this->_options, (array) $options);
	}

	/**
	 * Method to add a smiley replacement map
	 *
	 * @access	public
	 * @param	string	$string	The smiley string to replace, eg. :)
	 * @param	string	$image	The image name of the image to replace the smiley string.
	 * @return	void
	 * @since	1.0
	 */
	function addSmiley($string, $image)
	{
		$this->smilies[$string] = $image;
	}

	/**
	 * Method to parse the input string of BBCode and return processed XHTML
	 *
	 * @access	public
	 * @param	string	$input	BBCode input string.
	 * @return	string	XHTML representation of the BBCode input string.
	 * @since	1.0
	 */
	function parse($input)
	{
		// set the input string to the internal buffer for processing
		$this->_buffer = $input;

		// make sure all html is stripped from the buffer
		$this->_buffer = strip_tags($this->_buffer);

		// are code blocks present
		$code = false;

		// pre-process the [code] tags if enabled
		if (!empty($this->_options['enable_code']) and stristr($this->_buffer, '[code') !== false) {
			$this->_preProcessCodeBlocks();
			$code = true;
		}

		// process whitespace -- skipping over pre-processed sections
		$this->_processWhitespace();

		// post-process the [code] tags
		if ($code) {
			$this->_postProcessCodeBlocks();
		}

		// autoclose nestable tags
		$this->_autocloseTags();

		// if a [size] tag exists and simple formatting is enabled, process it
		if (!empty($this->_options['enable_simpleformat']) and stristr($this->_buffer, '[size=') !== false) {
			$size = array();
			$size['tag'] = 'size';
			$size['search'] = '#\[\x07=([\d]+)(?::\w+)?\]([^\x07]*)\[/\x07(?::\w+)?\]#esi';
			$size['replace'] = '"<span style=\"font-size:". $this->_sanitizePixelSize(\'$1\') ."px\">". str_replace(\'\"\', \'"\', \'$2\') ."</span>"';

			$this->_replaceNestTag($size);
		}

		// if a [color] tag exists and simple formatting is enabled, process it
		if (!empty($this->_options['enable_simpleformat']) and stristr($this->_buffer, '[color=') !== false) {
			$color = array();
			$color['tag'] = 'color';
			$color['search'] = '#\[\x07=([\#\w]+)(?::\w+)?\]([^\x07]*)\[/\x07(?::\w+)?\]#si';
			$color['replace'] = '<span style="color:$1">$2</span>';

			$this->_replaceNestTag($color);
		}

		// if a [font] tag exists and simple formatting is enabled, process it
		if (!empty($this->_options['enable_simpleformat']) and stristr($this->_buffer, '[font=') !== false) {
			$font = array();
			$font['tag'] = 'font';
			$font['search'] = '#\[\x07=([\w\s]+)(?::\w+)?\]([^\x07]*)\[/\x07(?::\w+)?\]#si';
			$font['replace'] = '<span style="font-family:$1">$2</span>';

			$this->_replaceNestTag($font);
		}

		// if a [list] tag exists and lists are enabled, process it
		if (!empty($this->_options['enable_lists']) and stristr($this->_buffer, '[list') !== false) {
			$this->_processLists();
		}

		// process BBCode tags
		$ex = $this->_getProcessExpressions();
		$this->_buffer = preg_replace(array_keys($ex), array_values($ex), $this->_buffer);

		// if extended formatting is enabled allow <hr />, <br /> and &nbsp; tags
		if (!empty($this->_options['enable_extendedformat'])) {
			$str = array (
				// Horizontal delimiter
				'[hr]' => '<hr />',
				// Force line break
				'[br]' => '<br />',
				// Force space
				'[sp]' => '&nbsp;'
			);
			$this->_buffer = str_replace(array_keys($str), array_values($str), $this->_buffer);
		}

		// process links
		$this->_processLinks();

		// add the nofollow attribute to <a> tags if enabled
		if (!empty($this->_options['nofollow_links'])) {
			$this->_buffer = preg_replace('#<a([^>]+)>#i', '<a\\1 rel="nofollow">', $this->_buffer);
		}

		// if smilies are enabled, convert them dynamically
		if (!empty($this->_options['enable_smilies'])) {
			$this->_parseSmilies();
		}

		return $this->_buffer;
	}

	function _preProcessCodeBlocks()
	{
		// initialize variables
		$this->_preprocessed = array();
		$i = 0;

		if (preg_match_all('#\[code(.+?)\](.*?)\[/code(?::\w+)?\]#si', $this->_buffer, $code_tags, PREG_SET_ORDER)) {
			foreach ($code_tags as $code_tag)
			{
				// show (X)HTML properly
				$code_tag[2] = str_replace(array('<','>'), array('&lt;','&gt;'), $code_tag[2]);

				// sanitize the language field
				$code_tag[1] = trim($code_tag[1], ' =');

				// strip pre-formatted code blocks from text during whitespace processing
				$this->_buffer = str_replace($code_tag[0], "***pRe_sTrInG$i***", $this->_buffer);
				$this->_preprocessed[$i++] = '<pre lang="'.$code_tag[1].'"><code>'.trim($code_tag[2]).'</code></pre>';
			}
		}
	}

	function _processWhitespace()
	{
		// sanitize silly line feeds and excessive line breaks
		$this->_buffer = preg_replace("/(\r\n|\n|\r)/", "\n", $this->_buffer);
		$this->_buffer = preg_replace("/\n\n+/", "\n\n", $this->_buffer);

		// process the whitespace
		switch ($this->_options['whitespace'])
		{
			case 2:
				// more advanced line break and paragraph separation logic...
				// TODO: this needs love!
				$parts = explode("\n\n", $this->_buffer);
				for ($i = 0; $i < sizeof($parts); $i++) {
					// No linebreaks if paragraph starts with an HTML tag
					if (!preg_match('/^<.*>/', $parts[$i]))
						$parts[$i] = nl2br($parts[$i]);

					// Some tags should not be in paragraph blocks
					if (!preg_match('/^(?:<|\[)(?:table|list|ol|ul|pre|select|form|blockquote|hr)/i', $parts[$i]))
						$parts[$i] = '<p>' . $parts[$i] . '</p>';
				}
				$this->_buffer = implode("\n\n", $parts);
				break;

			case 1:
				// simply to a new line to <br /> replace
				$this->_buffer = nl2br($this->_buffer);
				break;

			default:
				// easy mode :) -- do nothing
				break;
		}
	}

	function _postProcessCodeBlocks()
	{
		// re-insert pre-formatted code blocks
		foreach ($this->_preprocessed as $i => $code_tag)
		{
			$this->_buffer = str_replace("***pRe_sTrInG$i***", $code_tag, $this->_buffer);
		}
	}

	/**
	 * Method to autodiscover smilies in the input buffer and replace them with <img> tags for appropriate XHTML display
	 *
	 * @access	protected
	 * @return	void
	 * @since	1.0
	 */
	function _parseSmilies()
	{
		foreach ($this->smilies as $string => $image)
		{
			$this->_buffer = str_replace($string, '<img src="' . $this->_options['smileyPath'] . $image . '" alt="' . $string . '" />', $this->_buffer);
		}
	}

	/**
	 * Method to autoclose nestable tags that could cause XHTML validation issues
	 *
	 */
	function _autocloseTags()
	{
		// [quote] tag
		preg_match_all('/\[quote/i', $this->_buffer, $matches);
		$open = count($matches['0']);
		preg_match_all('/\[\/quote\]/i', $this->_buffer, $matches);
		$unclosed = $open -count($matches['0']);
		for ($i = 0; $i < $unclosed; $i++)
		{
			$this->_buffer .= '[/quote]';
		}

		// [list] tag
		preg_match_all('/\[list/i', $this->_buffer, $matches);
		$open = count($matches['0']);
		preg_match_all('/\[\/list\]/i', $this->_buffer, $matches);
		$unclosed = $open -count($matches['0']);
		for ($i = 0; $i < $unclosed; $i++)
		{
			$this->_buffer .= '[/list]';
		}

		return true;
	}

	function _replaceNestTag($tag = null)
	{
		$this->_buffer = preg_replace('#(\[[/]*)' . $tag['tag'] . '(.*?\])#si', "$1\x07$2", $this->_buffer);
		while (preg_match($tag['search'], $this->_buffer))
		{
			$this->_buffer = preg_replace($tag['search'], $tag['replace'], $this->_buffer);
		}
	}

	function _processLists()
	{
		// cleanup BS [/*] tags
		$this->_buffer = str_replace('[/*]', '', $this->_buffer);

		// prepare the list tags
		$this->_buffer = preg_replace('#(\[[/]*)list(.*?\])#si', "$1\x07$2", $this->_buffer);

		// replace to <li> tags - [*]..[*]|[*]..[/list]
		$this->_buffer = preg_replace('#\[\*(?::\w+)?\]([^\x07]*?)(?=\s*?(\[\*(?::\w+)?\]|\[/\x07(?::\w+)?\]))#si', '<li>$1</li>', $this->_buffer);
		// add </li> tags to nested <li> - [/list]..[/list]
		$this->_buffer = preg_replace('#(\[/\x07(?::\w+)?\])(?=[^\x07]*?\[/\x07(?::\w+)?\])#si', '$1</li>', $this->_buffer);
		// add </li> tags to nested <li> - [/list]..[*]..[list]
		$this->_buffer = preg_replace('#(\[/\x07(?::\w+)?\])(?=[^\x07]*?\[\*(?::\w+)?\][^\x07]*?\[\x07.*(?::\w+)?\])#si', '$1</li>', $this->_buffer);
		// replace to <li> tags for nested <li> - [*]..[list]
		$this->_buffer = preg_replace('#\[\*(?::\w+)?\]([^\x07]*)?(?=\[\x07.*(?::\w+)?\])#si', '<li>$1', $this->_buffer);

		// replace to <ol>/<ul> and </ol>/</ul> tags
		while (preg_match("#\[\x07[=]*((?-i)[cds1aAiI])*(?::\w+)?\]([^\x07]*)\[/\x07(?::\w+)?\]#si", $this->_buffer))
		{
			$this->_buffer = preg_replace_callback("#\[\x07[=]*((?-i)[cds1aAiI])*(?::\w+)?\]([^\x07]*)\[/\x07(?::\w+)?\]#si", array($this,'_processListTags'), $this->_buffer);
		}

		// remove <br /> tags
		$this->_buffer = preg_replace('#(<[/]*([uo]l|li).*>.*)<br />#i', '$1', $this->_buffer);

		return true;
	}

	function _processListTags($matches)
	{
		// array of list types
		$type = array (
			null => array (
				'style' => null,
				'tag' => 'ul'
			),
			'c' => array (
				'style' => 'circle',
				'tag' => 'ul'
			),
			'd' => array (
				'style' => 'disc',
				'tag' => 'ul'
			),
			's' => array (
				'style' => 'square',
				'tag' => 'ul'
			),
			'1' => array (
				'style' => 'decimal',
				'tag' => 'ol'
			),
			'a' => array (
				'style' => 'lower-alpha',
				'tag' => 'ol'
			),
			'A' => array (
				'style' => 'upper-alpha',
				'tag' => 'ol'
			),
			'i' => array (
				'style' => 'lower-roman',
				'tag' => 'ol'
			),
			'I' => array (
				'style' => 'upper-roman',
				'tag' => 'ol'
			)
		);
		$style = ($matches[1]) ? ' style="list-style-type:"'.$type[$matches[1]]['style'].';"' : null;
		return '<'.$type[$matches[1]]['tag'].$style.'>'.str_replace('\"', '"', $matches[2]).'</'.$type[$matches[1]]['tag'].">\n";
	}

	function _processLinks()
	{

		// We cannot evaluate the variable in callback function because
		// there is no way to pass the $format variable
		if (!empty($this->_options['enable_emailcloaking'])) {
			// Replacing email addresses with encoded html
			$this->_buffer = preg_replace_callback('#\[email(?::\w+)?\]([\w\.\-\+~@]+)\[/email(?::\w+)?\]#si', array($this,'_cloakEmailAddress'), $this->_buffer);
			$this->_buffer = preg_replace_callback('#\[email=(.*?)(?::\w+)?\](.*?)\[/email(?::\w+)?\]#si', array($this,'_cloakEmailAddress'), $this->_buffer);
		} else {
			$this->_buffer = preg_replace(array(
				'#\[email(?::\w+)?\](.*?)\[/email(?::\w+)?\]#si',
				'#\[email=(.*?)(?::\w+)?\]([\w\s]+)\[/email(?::\w+)?\]#si'
			), array(
				'<a href="mailto:\\1">\\1</a>',
				'<a href="mailto:\\1">\\2</a>'
			), $this->_buffer);
		}

		// Turns web and e-mail addresses into clickable links
		if (!empty($this->_options['enable_autolinks'])) {

			// pad with a space so we can match things at the start of the 1st line
			$ret = ' ' . $this->_buffer;
			// padding to already filtered links
			$ret = preg_replace('#(<a.+>)(.+</a>)#i', "$1\x07$2", $ret);

			// matches an "xxx://yyyy" URL at the start of a line, or after a space.
			// xxxx can only be alpha characters.
			// yyyy is anything up to the first space, newline, comma, double quote or <
			$ret = preg_replace('#(?<=^|[\t\r\n >\(\[\]\|])([a-z]+?://[\w\-]+\.([\w\-]+\.)*\w+(:[0-9]+)?(/[^ "\'\(\n\r\t<\)\[\]\|]*)?)((?<![,\.])|(?!\s))#i', '<a href="\1">\1</a>', $ret);

			// matches a "www|ftp.xxxx.yyyy[/zzzz]" kinda lazy URL thing
			// Must contain at least 2 dots. xxxx contains either alphanum, or "-"
			// zzzz is optional.. will contain everything up to the first space, newline,
			// comma, double quote or <.
			$ret = preg_replace('#([\t\r\n >\(\[\|])(www|ftp)\.(([\w\-]+\.)*[\w]+(:[0-9]+)?(/[^ \"\'\(\n\r\t<\)\[\]\|]*)?)#i', '\1<a href="http://\2.\3">\2.\3</a>', $ret);

			// matches an email@domain type address at the start of a line, or after a space.
			// Note: Only the followed chars are valid; alphanums, "-", "_" and or ".".
			if (!empty($this->_options['enable_emailcloaking'])) {
				$ret = preg_replace_callback("#([\t\r\n ])([a-z0-9\-_.]+?)@([\w\-]+\.([\w\-\.]+\.)*[\w]+)#i", array($this,'_cloakEmailAddress'), $ret);
			} else {
				$ret = preg_replace('#([\t\r\n ])([a-z0-9\-_.]+?)@([\w\-]+\.([\w\-\.]+\.)*[\w]+)#i', '\\1<a href="mailto:\\2@\\3">\\2@\\3</a>', $ret);
			}

			// Remove our padding
			$ret = str_replace("\x07", '', $ret);
			$this->_buffer = substr($ret, 1);
		}
	}

	function _getProcessExpressions()
	{
		// if the process expressions are already compiled just return them
		if (!empty($this->_expressions)) {
			return $this->_expressions;
		}

		// the base expression for BBCode will support the notag tag
		$this->_expressions = array('#\[notag(?::\w+)?\](.*?)\[/notag(?::\w+)?\]#sie' => '$this->_encodeNoTagSegment(\'\\1\')');

		// should we enable simple text formatting tags?
		if (!empty($this->_options['enable_simpleformat'])) {
			$this->_expressions = array_merge($this->_expressions, array(
				'#\[(b|strong)(?::\w+)?\](.*?)\[/(b|strong)(?::\w+)?\]#si' => '<strong>\\2</strong>',
				'#\[(i|em)(?::\w+)?\](.*?)\[/(i|em)(?::\w+)?\]#si' => '<em>\\2</em>',
				'#\[u(?::\w+)?\](.*?)\[/u(?::\w+)?\]#si' => '<ins>\\1</ins>',
				'#\[s(?::\w+)?\](.*?)\[/s(?::\w+)?\]#si' => '<del>\\1</del>',
				'#\[sup(?::\w+)?\](.*?)\[/sup(?::\w+)?\]#si' => '<sup>\\1</sup>',
				'#\[sub(?::\w+)?\](.*?)\[/sub(?::\w+)?\]#si' => '<sub>\\1</sub>'
			));
		}

		// should we allow extended formatting tags for alignment and floating?
		if (!empty($this->_options['enable_extendedformat'])) {
			$this->_expressions = array_merge($this->_expressions, array(
				'#\[align=(\w+)(?::\w+)?\](.*?)\[/align(?::\w+)?\]#si' => '<span style="text-align:\\1">\\2</span>',
				'#\[float=(left|right)(?::\w+)?\](.*?)\[/float(?::\w+)?\]#si' => '<span style="float:\\1">\\2</span>',
				'#\[justify(?::\w+)?\](.*?)\[/justify(?::\w+)?\]#si' => '<div style="text-align:justify;">\\1</div>',
				'#\[center(?::\w+)?\](.*?)\[/center(?::\w+)?\]#si' => '<div style="text-align:center">\\1</div>',
				'#\[left(?::\w+)?\](.*?)\[/left(?::\w+)?\]#si' => '<div style="text-align:left">\\1</div>',
				'#\[right(?::\w+)?\](.*?)\[/right(?::\w+)?\]#si' => '<div style="text-align:right">\\1</div>'
			));
		}

		// should we allow url tags
		if (!empty($this->_options['enable_urls'])) {
			$this->_expressions = array_merge($this->_expressions, array(
				'#\[url(?::\w+)?\]www\.([\w:;&,%+~!=@\/\.\-\#\?]+?)\[/url(?::\w+)?\]#si' => '<a href="http://www.\\1">\\1</a>',
				'#\[url(?::\w+)?\]([\w:;&,%+~!=@\/\.\-\#\?]+?)\[/url(?::\w+)?\]#si' => '<a href="\\1">\\1</a>',
				'#\[url=www\.([\w:;&,%+~!=@\/\.\-\#\?]+?)\](.*?)\[/url(?::\w+)?\]#si' => '<a href="http://www.\\1">\\2</a>',
				'#\[url=([\w:;&,%+~!=@\/\.\-\#\?]+?)\](.*?)\[/url(?::\w+)?\]#si' => '<a href="\\1">\\2</a>'
			));
		}

		// should we allow quoting?
		if (!empty($this->_options['enable_quotes'])) {
			$this->_expressions = array_merge($this->_expressions, array(
				'#\[quote(?::\w+)?\]#i' => '<div class="quotation">'.JText::_('Quote:').'<blockquote>',
				'#\[quote=(?:&quot;|"|\')?(.*?)["\']?(?:&quot;|"|\')?\]#i' => '<div class="quotation"><cite>'.JText::_('\\1 wrote:').'</cite><blockquote>',
				'#\[/quote(?::\w+)?\]#si' => '</blockquote></div>'
			));
		}

		// should we allow abbreviations?
		if (!empty($this->_options['enable_abbreviations'])) {
			$this->_expressions = array_merge($this->_expressions, array(
				'#\[acronym=([\w\s-,\.]+)(?::\w+)?\](.*?)\[/acronym(?::\w+)?\]#si' => '<acronym title="\\1">\\2</acronym>',
				'#\[abbr=([\w\s-,\.]+)(?::\w+)?\](.*?)\[/abbr(?::\w+)?\]#si' => '<abbr title="\\1">\\2</abbr>'
			));
		}

		// should we allow anchor tags?
		if (!empty($this->_options['enable_anchors'])) {
			$this->_expressions = array_merge($this->_expressions, array(
				'#\[anchor=(\w+)(?::\w+)?\](.*?)\[/anchor(?::\w+)?\]#si' => '<a name="\\1">\\2</a>',
			));
		}

		// should we allow php code blocks?
		if (!empty($this->_options['enable_php'])) {
			$this->_expressions = array_merge($this->_expressions, array(
				'#\[php(?::\w+)?\](?:[\r\n])*(.*?)\[/php(?::\w+)?\]#sie' => '$this->_processPHPCodeBlock(\'\\1\')',
			));
		}

		// should we allow google tags for google search pages?
		if (!empty($this->_options['enable_google'])) {
			$this->_expressions = array_merge($this->_expressions, array(
				'#\[google(?::\w+)?\]([\w\s-]+?)\[/google(?::\w+)?\]#si' => '<a href="http://www.google.com/search?q=\\1">\\1</a>'
			));
		}

		// should we allow wikipedia tags for wikipedia pages?
		if (!empty($this->_options['enable_wikipedia'])) {
			$this->_expressions = array_merge($this->_expressions, array(
				'#\[wikipedia(?::\w+)?\]([\w\s-]+?)\[/wikipedia(?::\w+)?\]#si' => '<a href="http://www.wikipedia.org/wiki/\\1">\\1</a>'
			));
		}

		// should we allow youtube tags for inserting youtube movies?
		if (!empty($this->_options['enable_youtube'])) {
			$this->_expressions = array_merge($this->_expressions, array(
				'#\[youtube\]([0-9a-zA-Z]+)\[/youtube\]#si' => '<object width="425" height="366"><param name="movie" value="http://www.youtube.com/v/\\1"></param><embed src="http://www.youtube.com/v/\\1" type="application/x-shockwave-flash" width="425" height="366"></embed></object>'
			));
		}

		// should we process image tags?
		if (!empty($this->_options['enable_images'])) {
			$this->_expressions = array_merge($this->_expressions, array(
				'#\[img(?::\w+)?\]([\w:;&,~%+!=@\/\.\-\#\?]+)\[/img(?::\w+)?\]#si' => '<img src="\\1" alt="" />',
				'#\[img=(\d+)x(\d+)(?::\w+)?\]([\w:;&,~%+!=@\/\.\-\#\?]+)\[/img(?::\w+)?\]#si' => '<img width="\\1" height="\\2" alt="" src="\\3" />',
				'#\[img=([\w\s:;,\.\-\'\(\)]+)(?::\w+)?\]([\w:;&,~%+!=@\/\.\-\#\?]+)\[/img(?::\w+)?\]#si' => '<img alt="\\1" src="\\2" />',
				'#\[img align=(left|right|center)(?::\w+)?\]([\w:;&,~%+!=@\/\.\-\#\?]+)\[/img(?::\w+)?\]#si' => '<img src="\\2" alt="" align="\\1" />'
			));
		}

		// should we process flash tags for animations and other special effects?
		if (!empty($this->_options['enable_flash'])) {
			$this->_expressions = array_merge($this->_expressions, array(
				'#\[flash=(\d+)x(\d+)(?::\w+)?\]([\w:;&,~%+!=@\/\.\-\#\?]+)\[/flash(?::\w+)?\]#si' => '<object type="application/x-shockwave-flash" data="\\3" width="\\1" height="\\2"><param name="movie" value="\\3" /></object>'
			));
		}

		// should we process table tags and related sub-tags?
		if (!empty($this->_options['enable_tables'])) {
			$this->_expressions = array_merge($this->_expressions, array(
				'#\[table\](.+?)\[/table\]#si' => '<table>\\1</table>',
				'#\[(row|r|tr)\](.+?)\[/(row|r|tr)\]#si' => '<tr>\\2</tr>',
				'#\[(row|r|tr) color=([\#\w]+)\](.+?)\[/(row|r|tr)\]#si' => '<tr bgcolor=\\2>\\3</tr>',
				'#\[(header|head|h)\](.+?)\[/(header|head|h)\]#si' => '<th>\\2</th>',
				'#\[(col|c|td)\](.+?)\[/(col|c|td)\]#si' => '<td valign="top">\\2</td>',

				// cleanup table output (td, th and tr tags)
				'#<([\/]?)t([dhr])><br />#si' => '<\\1t\\2>',
				'#<table(.+?)><br />#si' => '<table\\1>'
			));
		}

		return $this->_expressions;
	}

	function _encodeNoTagSegment($text = null)
	{
		return str_replace(array('[',']','@'), array('&#91;','&#93;','&#64;'), stripslashes($text));
	}

	function _sanitizePixelSize($size)
	{
		// no smaller than 6 pixels
		$size = ($size < 6) ? 6 : $size;

		// no larger than 48 pixels
		$size = ($size > 48) ? 48 : $size;

		return $size;
	}

	function _processPHPCodeBlock($text = null)
	{
		return '<pre lang="php"><code>'.trim(str_replace('<br />', '', stripslashes($text))).'</code></pre>';
	}

	function _cloakEmailAddress($matches) {
		if (isset ($matches[3]))
			$link = 'document.write(\'<a href="mailto:' . $matches[2] . '@' . $matches[3] . '">' . $matches[2] . '@' . $matches[3] . '</a>\');';
		else
			$link = 'document.write(\'<a href="mailto:' . $matches[1] . '">' . (isset ($matches[2]) ? $matches[2] : $matches[1]) . '</a>\');';

		$js_encode = '';
		for ($x = 0; $x < strlen($link); $x++)
			$js_encode .= '%' . bin2hex($link {
			$x });

		$link = '<script type="text/javascript">eval(unescape(\''.$js_encode.'\'))</script>';
		if (isset ($matches[3]))
			$link = $matches[1] . $link;

		return $link;
	}
}
